{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 线性回归的简洁实现\n",
    ":label:`sec_linear_concise`\n",
    "\n",
    "在过去的几年里，出于对深度学习强烈的兴趣，许多公司、学者和业余爱好者开发了各种成熟的开源框架。通过这些框架可以自动化实现基于梯度的学习算法中重复性的工作。\n",
    "在 :numref:`sec_linear_scratch` 中，我们只依赖了：（1）通过`NDArray`来进行数据存储和线性代数；（2）通过`GradientCollector`来计算梯度。实际上，由于数据迭代器、损失函数、优化器和神经网络层很常用，现代深度学习库也为我们实现了这些组件。\n",
    "\n",
    "在本节中，我们将介绍如何通过使用 DJL 框架来简洁地实现 :numref:`sec_linear_scratch` 中的线性回归模型。\n",
    "\n",
    "## 生成数据\n",
    "\n",
    "与 :numref:`sec_linear_scratch` 中类似，我们首先生成数据。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load ../utils/djl-imports\n",
    "%load ../utils/DataPoints.java\n",
    "%load ../utils/Training.java"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "NDManager manager = NDManager.newBaseManager();\n",
    "\n",
    "NDArray trueW = manager.create(new float[]{2, -3.4f});\n",
    "float trueB = 4.2f;\n",
    "\n",
    "DataPoints dp = DataPoints.syntheticData(manager, trueW, trueB, 1000);\n",
    "NDArray features = dp.getX();\n",
    "NDArray labels = dp.getY();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 读取数据集\n",
    "\n",
    "首先我们使用 `features` 和 `labels` 来创建一个 `ArrayDataset` 数据集，并在实例化时指定 `batchSize`。此外，布尔值 `shuffle` 表示是否希望数据迭代器对象在每个迭代周期内打乱数据。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "// Saved in the utils file for later use\n",
    "public ArrayDataset loadArray(NDArray features, NDArray labels, int batchSize, boolean shuffle) {\n",
    "    return new ArrayDataset.Builder()\n",
    "                  .setData(features) // set the features\n",
    "                  .optLabels(labels) // set the labels\n",
    "                  .setSampling(batchSize, shuffle) // set the batch size and random sampling\n",
    "                  .build();\n",
    "}\n",
    "\n",
    "int batchSize = 10;\n",
    "ArrayDataset dataset = loadArray(features, labels, batchSize, false);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用 `ArrayDataset` 的方式与我们在 :numref:`sec_linear_scratch` 中使用 `dataset.getData()` 函数的方式相同。为了验证是否正常工作，让我们读取并打印第一个小批量样本。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Batch batch = dataset.getData(manager).iterator().next();\n",
    "NDArray X = batch.getData().head();\n",
    "NDArray y = batch.getLabels().head();\n",
    "System.out.println(X);\n",
    "System.out.println(y);\n",
    "batch.close();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型\n",
    "\n",
    "当我们在 :numref:`sec_linear_scratch` 中实现线性回归时，我们明确定义了模型参数变量，并编写了计算的代码，这样通过基本的线性代数运算得到输出。但是，如果模型变得更加复杂，而且当你几乎每天都需要实现模型时，你会想简化这个过程。这种情况类似于从头开始编写自己的博客。做一两次是有益的、有启发性的，但如果每次你每需要一个博客就花一个月的时间重新发明轮子，那你将是一个糟糕的网页开发者。\n",
    "\n",
    "对于标准操作，我们可以 DJL 预定义的 `Block`。这使我们只需关注使用哪些层来构造模型，而不必关注层的实现细节。我们首先定义一个模型变量`net`，它是一个 `SequentialBlock` 类的实例。 `SequentialBlock` 类为串联在一起的多个层定义了一个容器。当给定输入数据， `SequentialBlock` 实例将数据传入到第一层，然后将第一层的输出作为第二层的输入，依此类推。在下面的例子中，我们的模型只包含一个层，因此实际上不需要`SequentialBlock`。但是由于以后几乎所有的模型都是多层的，在这里使用`SequentialBlock`会让你熟悉标准的流水线。\n",
    "\n",
    "回顾 :numref:`fig_single_neuron` 中的单层网络架构，这一单层被称为 *全连接层*（fully-connected layer），因为它的每一个输入都通过矩阵-向量乘法连接到它的每个输出。\n",
    "\n",
    "![Linear regression is a single-layer neural network. ](https://resources.djl.ai/d2l-java/singleneuron.svg)\n",
    ":label:`fig_singleneuron`\n",
    "\n",
    "在 DJL 中，全连接层在 `Linear` 类中定义。由于我们只想得到一个标量输出，所以我们将该数字设置为 1，并制定是否包含偏差（`bias`）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Model model = Model.newInstance(\"lin-reg\");\n",
    "\n",
    "SequentialBlock net = new SequentialBlock();\n",
    "Linear linearBlock = Linear.builder().optBias(true).setUnits(1).build();\n",
    "net.add(linearBlock);\n",
    "\n",
    "model.setBlock(net);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义损失函数\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在 DJL 中，抽象类 `Loss` 定义了损失函数的接口。在这个例子中，我们将使用平方损失 (`L2Loss`)。\n",
    "\n",
    "$$\n",
    "L2Loss = \\sum_{i = 1}^{n}(y_i - \\hat{y_i})^2\n",
    "$$\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Loss l2loss = Loss.l2Loss();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义优化算法\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "小批量随机梯度下降算法是一种优化神经网络的标准工具，DJL 通过 `Optimizer` 类支持该算法的许多变种。当我们实例化 `Optimizer` 时，我们要指定优化的参数。我们希望使用的优化算法（`sgd`）以及优化算法所需的超参数字典。小批量随机梯度下降只需要设置 `learningRate`值，这里设置为 0.03。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "Tracker lrt = Tracker.fixed(0.03f);\n",
    "Optimizer sgd = Optimizer.sgd().setLearningRateTracker(lrt).build();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `Trainer`的初始化配置\n",
    "\n",
    "下面这段程序，展示了我们如何初始化及配置`trainer`，并用这个`trainer`对人工智能模型进行训练。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "DefaultTrainingConfig config = new DefaultTrainingConfig(l2loss)\n",
    "    .optOptimizer(sgd) // Optimizer (loss function)\n",
    "    .optDevices(manager.getEngine().getDevices(1)) // single GPU\n",
    "    .addTrainingListeners(TrainingListener.Defaults.logging()); // Logging\n",
    "\n",
    "Trainer trainer = model.newTrainer(config);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 初始化模型参数\n",
    "\n",
    "在对人工智能模型进行训练前，我们需要对模型的参数进行初始化设置。例如，对线性回归模型进行初始化配置时，我们需要提供权重及偏差参数。在`DJL`里，进行这种初始化参数的配置，你只需要简单地调用 `initialize` 函数，将你希望使用的模型及模型参数传人`initialize` 函数即可。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "// First axis is batch size - won't impact parameter initialization\n",
    "// Second axis is the input size\n",
    "trainer.initialize(new Shape(batchSize, 2));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 运行性能指标\n",
    "\n",
    "一般情况下，DJL 不会自动记录运行性能指标，因为记录运行性能指标本身会提高运行成本，降低运行性能。如果出于特殊理由，你需要对某些运行指标进行记录，你可以生成一个 `metrics` 并将这个新生成的 `metrics` 设置成 `trainer` 的 `metrics` 即可，具体程序如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Metrics metrics = new Metrics();\n",
    "trainer.setMetrics(metrics);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "通过 DJL 来实现我们的模型只需要相对较少的代码。我们不必单独分配参数、不必定义我们的损失函数，也不必手动实现小批量随机梯度下降。当我们需要更复杂的模型时，DJL 的优势将大大增加。当我们有了所有的基本组件，训练过程代码与我们从零开始实现时所做的非常相似。\n",
    "\n",
    "回顾一下：在每个迭代周期里，我们将完整遍历一次数据集（`dataset`），不停地从中获取一个小批量的输入和相应的标签。对于每一个小批量，我们会进行以下步骤:\n",
    "\n",
    "* 通过调用 `trainBatch(batch)` 生成预测并计算损失（正向传播）并计算梯度（反向传播）。\n",
    "* 通过调用 `step` 函数来更新模型参数。\n",
    "\n",
    "训练中`Trainer`会自动打印损失和精确度到日志文件。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "int numEpochs = 3;\n",
    "\n",
    "for (int epoch = 1; epoch <= numEpochs; epoch++) {\n",
    "    System.out.printf(\"Epoch %d\\n\", epoch);\n",
    "    // Iterate over dataset\n",
    "    for (Batch batch : trainer.iterateDataset(dataset)) {\n",
    "        // Update loss and evaulator\n",
    "        EasyTrain.trainBatch(trainer, batch);\n",
    "        \n",
    "        // Update parameters\n",
    "        trainer.step();\n",
    "        \n",
    "        batch.close();\n",
    "    }\n",
    "    // reset training and validation evaluators at end of epoch\n",
    "    trainer.notifyListeners(listener -> listener.onEpoch(trainer));\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面，我们将真实原始参数（权重`trueW`和偏差`trueB`）和模型训练中学习到的参数（`wParam`和`bParam`）进行比较。\n",
    "\n",
    "在DJL里，访问模型训练中学习到的参数需要分两步走。从模型`model`中取出构建层`layer`。从构建层中用`getParameters()`函数取参数列表。取得参数列表以后，每个独立的参数就可以通过`valueAt()`函数用列表下标获取了。下面的例子中，权重及偏差参数分别可以用列表下标 0 `valueAt(0)` 和下标 1 `valueAt(1)` 获取。\n",
    "\n",
    "这个实验案例的参数对比结果显示，模型训练中学习到的参数和真实原始参数是非常接近的。说明这个训练模型是成功有效的。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Block layer = model.getBlock();\n",
    "ParameterList params = layer.getParameters();\n",
    "NDArray wParam = params.valueAt(0).getArray();\n",
    "NDArray bParam = params.valueAt(1).getArray();\n",
    "\n",
    "float[] w = trueW.sub(wParam.reshape(trueW.getShape())).toFloatArray();\n",
    "System.out.printf(\"Error in estimating w: [%f %f]\\n\", w[0], w[1]);\n",
    "System.out.println(String.format(\"Error in estimating b: %f\\n\", trueB - bParam.getFloat()));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 保存训练模型\n",
    "\n",
    "通过上面的步骤，当训练出一个满意的模型，你可以将这个训练模型保存下来，用在将来的人工智能项目中。\n",
    "训练模型保存除了保存模型本身，模型的元数据也应该保存（如模型精确度，训练周期等）。我们可以使用 `setProperty()` 函数设置元数据。\n",
    "DJL中保存模型非常简单，用 `Paths.get()` 函数指定一个模型保存路径，调用 `save()` 函数，模型及模型的元数据就会一起保存到你指定的文件目录里。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Path modelDir = Paths.get(\"../models/lin-reg\");\n",
    "Files.createDirectories(modelDir);\n",
    "\n",
    "model.setProperty(\"Epoch\", Integer.toString(numEpochs)); // save epochs trained as metadata\n",
    "\n",
    "model.save(modelDir, \"lin-reg\");\n",
    "\n",
    "model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 小结\n",
    "\n",
    "* 我们可以使用 DJL 更简洁地实现模型。\n",
    "* 在DJL项目里，`training.dataset`包里有不少数据处理工具，`nn` 包里定义了大量的神经网络构建层 `layer` ，`Loss` 类定义了许多通用的损失函数。学员们可以根据自己的需要到这些包和类里找合用的工具及函数。\n",
    "* DJL的 `training.initializer` 包里有各种模型初始化方法及相应的初始化配置参数供学员参考。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 练习\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. 参照 DJL 文档，找出其他的损失函数和初始化的方法，使用 `L1` 损失函数替换 `L2Loss`。\n",
    "1. 你如何在训练中访问模型的参数？\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Java",
   "language": "java",
   "name": "java"
  },
  "language_info": {
   "codemirror_mode": "java",
   "file_extension": ".jshell",
   "mimetype": "text/x-java-source",
   "name": "Java",
   "pygments_lexer": "java",
   "version": "14.0.2+12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
